{% extends 'website/src/base.html' %}
{% load calc %}
{% load static %}
{% block style %}
    <style>
        /* 初始按钮样式 */
        #toggleMenuBtn {
            padding: 10px 20px;
            background: #4a86e8;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 16px;
        }

        /* 下拉菜单容器 - 初始隐藏 */
        .dropdown-container {
            display: none;
            position: absolute;
            background: white;
            border: 1px solid #ddd;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            z-index: 1000;
            min-width: 200px;
        }

        /* 主选项样式 */
        .menu-item {
            padding: 10px 15px;
            position: relative;
            border-bottom: 1px solid #eee;
            cursor: pointer;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }

        .menu-item:hover {
            background-color: #f5f5f5;
        }

        /* 子菜单样式 - 初始隐藏 */
        .sub-menu {
            display: none;
            position: absolute;
            top: 0;
            left: 100%;
            background: white;
            border: 1px solid #ddd;
            box-shadow: 2px 2px 5px rgba(0,0,0,0.1);
            min-width: 150px;
        }

        /* 鼠标悬停时显示子菜单 */
        .menu-item:hover .sub-menu {
            display: block;
        }

        .sub-option {
            padding: 8px 12px;
            cursor: pointer;
        }

        .sub-option:hover {
            background-color: #e9e9e9;
        }

        .chart-btn {
          padding: 8px 16px;
          margin: 0 10px;
          background: #1890ff;
          color: white;
          border: none;
          border-radius: 4px;
          cursor: pointer;
        }

        .chart-btn:hover {
          background: #40a9ff;
        }
        .chart-card {
            height: 78vh; /* 或具体像素值 */
            display: flex;
            flex-direction: column;
            border-radius: 8px;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
            padding: 15px;
            background: #222;
        }
        #lineChart {
            width: 100%;
            {#height: 400px;#}
            height: 100%; /* 关键修改 */
            min-height: 300px; /* 设置最小高度防止内容过少时变形 */
        }
        /* 定制滑块 */
        input[type=range]::-webkit-slider-thumb {
            -webkit-appearance: none; /* 移除默认样式 */
            height: 20px; /* 指定高度 */
            width: 25px; /* 指定宽度 */
            background-color: black; /* 指定背景颜色 */
            border-radius: 1%; /* 圆形滑块 */
        {#border: solid 0 white; /* 边框 */ cursor: pointer; /* 鼠标样式为指针 */ #}
            margin-top: -10px; /* 垂直居中 */
            /* 创建三角形指针 */
            background-clip: content-box;
            clip-path: polygon(50% 0, 100% 100%, 0 100%);
        }

        input[type=range]::-webkit-slider-runnable-track {
            width: 100%; /* 轨道宽度 */
            height: 10px; /* 轨道高度 */
            background: #313438; /* 轨道颜色 */
            border-radius: 10px; /* 轨道圆角 */
        }

        input[type=range]::-moz-range-thumb {
            height: 20px;
            width: 25px;
            background-color: #00f;
            border-radius: 1%;
            border: solid 2px white;
            margin-top: -10px;
            /* 创建三角形指针 */
            background-clip: content-box;
            clip-path: polygon(50% 0, 100% 100%, 0 100%);
        }

        {#input[type=range] {#}
        {#  width: 100%; /* 指定宽度 */#}
        {#  margin: 20px 0; /* 上下外边距 */#}
        {# }#}

        .input-group-prepend .btnA,
        .input-group-append .btnA {
            height: 30px;
            width: 30px;
            background-color: #2c2d2f;
            color: #00aab7; /* 调整按钮颜色 */
            border: 1px solid #00aab7;
        }

        .input-group-prepend .btnA {
            border-radius: 5px 0 0 5px;
            font-size: 14px; /* 调整按钮大小 */
        }

        .input-group-append .btnA {
            border-radius: 0 5px 5px 0;
            font-size: 14px; /* 调整按钮大小 */
        }

        .form-controlA {
            height: 30px;
            width: 70px;
            font-size: 18px; /* 调整输入框大小 */
            color: #ff6d05; /* 调整输入框颜色 */
            background-color: #2c2d2f;
            border: 1px solid #00aab7;
            margin: 0;
        }

        .form-controlA:focus {
            color: yellow; /* 调整输入框颜色 */
            background-color: #2c2d2f;
            border: 1px solid #00aab7;
        }

        .form-control-sm {
            color: #ff6d05; /* 调整输入框颜色 */
            height: 35px;
            width: 128px;
            font-size: 18px;
            padding-top: 2px;
            background-color: #2c2d2f;
            border: 1px solid #00aab7;
            margin-left: 0;
        }

        .form-control-sm:focus {
            color: yellow; /* 调整输入框颜色 */
            background-color: #2c2d2f;
            border: 1px solid #00aab7;
        }

        .checkboxInput{
            content: '';
            display: inline-block;
            width: 20px;
            height: 20px;
            margin-left: 8px;
            margin-right: 8px;
            border: 2px solid #ccc;
            vertical-align: middle;
            cursor: pointer;
        }

    </style>
{% endblock %}

{% block main %}
    <div class="row m-3 container-fluid" style="border-radius: 10px; border: 1px solid #313438; background-color: #2c2d2f">
        <!-- 左边设备数据 -->
        <div class="col-lg-4">  <!-- 先用整个面板的1/3 -->
            <div class="row">
                <label class="text-info text-center fs-5 mt-2" id="textInfo">{{ alias.name }} -> {{ alias.iotInstanceId_alias }}
                    -> {{ alias.product_key_alias }} -> {{ alias.device_name_alias }}</label>
            </div>
            <form id="funData" class="form-horizontal" method="post" novalidate style="border: none">
                {% csrf_token %}
                {% for fun in toFun %}
                    <div class="row mt-3 d-flex flex-nowrap checkbox-item">
                        <div class="col-5 text-end" style="padding: 0 10px 0 0;">
                            <label class="text-success ">{{ fun.funcName }}({{ fun.get_unit_display }}):</label>
                        </div>
                        <div class="col-6 text-start d-flex m-0 p-0">
                            <!-- 如果是数值类型  style="border: red 1px solid;"-->
                            {% if fun.dataType <= 3 %}
{#                                <div class="input-group">#}
                                  <span class="input-group-prepend">
                                    <button class="btnA" type="button" id="button-minus{{ fun.Identifier }}"
                                            onclick="sliderValue('{{ fun.Identifier }}', '-', '{{ fun.max }}', '{{ fun.min }}', '{{ fun.step }}')">━</button>
                                  </span>
                                  <input name="waitSubmit" class="form-controlA text-center" type="text"
                                           aria-label="Slider value" id="input-slider{{ fun.Identifier }}"
                                           value="{{ fun.value }}"
                                           onfocus="sliderValue('{{ fun.Identifier }}', '', '{{ fun.max }}', '{{ fun.min }}', '{{ fun.step }}')"
                                           onblur="inputOnblur('{{ fun.Identifier }}', '', '{{ fun.max }}', '{{ fun.min }}', '{{ fun.value }}', '{{ fun.step }}')"
                                    >
                                  <span class="input-group-append">
                                    <button class="btnA" type="button" id="button-plus{{ fun.Identifier }}"
                                            onclick="sliderValue('{{ fun.Identifier }}', '+', '{{ fun.max }}', '{{ fun.min }}', '{{ fun.step }}')">✚</button>
                                  </span>
                                    {# <p class="text-gray m-1 fs-14">原:{{ fun.value }}</p>#}
{#                                </div>#}
                                <!-- 滑动块，暂时不用 -->
                                {# <input type="range" class="form-range custom-range" min="0" max="100" id="slider{{ fun.Identifier }}" value="{{ fun.value }}" onfocus="sliderValue('{{ fun.Identifier }}')">#}
                            {% endif %}
                            <!-- 如果是enum枚举类型 或者 布尔类型 -->
                            {% if fun.dataType == 4 or fun.dataType == 5 %}
                                <div class="form-group-sm me-0">
                                    <label>
                                        <select name="waitSubmit" type="checkbox" class="form-control-sm"
                                                id="select{{ fun.Identifier }}">
                                            {% for key,value in fun.dataDefine.items %}
                                                {% if fun.value|floatformat:0 == key|floatformat:0 %}
                                                    <option selected>{{ key }} ~ {{ value }}</option>
                                                {% else %}
                                                    <option>{{ key }} ~ {{ value }}</option>
                                                {% endif %}
                                            {% endfor %}
                                        </select>
{#                                        <span class="text-gray m-1 fs-14">原:{{ fun.value }}</span>#}
                                    </label>
                                </div>
                            {% endif %}
                            {% if fun.dataType >= 6 %}
                                <label class="me-0">
                                    <input name="waitSubmit" type="text" class="form-control-sm pt-2"
                                           value="{{ fun.value }}" id="{{ fun.Identifier }}"/>
                                </label>
                                {# <span class="text-gray m-1 fs-14">原:{{ fun.value }}</span> #}
                            {% endif %}

                            <label class="checkbox-item">
                                <input type="checkbox" name="language" class='checkboxInput' checked />
                            </label>
                        </div>

                    </div>

                    {% empty %}
                    <div>没有可显示的内容</div>
                {% endfor %}

                <!-- 上报服务器  -->
                <div class="panel-footer text-center mt-3" >
                    <button type="button" class="btn btn-outline-danger border-1 border-light p-2 w-40 mx-2"
                        onclick="sendAction(1)">上报给物模型
                    </button>
                    <button type="button" class="btn btn-outline-danger border-1 border-light p-2 w-40 mx-2"
                        onclick="sendAction(2)">上报给设备
                    </button>

                </div>
                <p id="error1" class="mt-2 text-danger text-center fs-6"></p>
            </form>
        </div>

        <!-- 右边折线图 -->
        <div class="col-lg-8 container mt-4">
        <div class="row">
            <div class="col-md-12">
                <div class="chart-card">
                    <div id="lineChart"></div>
                      <!-- 按钮组 -->
                      <div style="text-align: center;margin-top: 15px">
                        <button id="reportDataBtn" class="chart-btn">查看上报数据</button>
                        <button id="receiveDataBtn" class="chart-btn">查看接收数据</button>
                        <!-- 初始按钮 -->
                        <button id="toggleMenuBtn">查看单个属性</button>
                        <!-- 下拉菜单容器 -->
                        <div id="dropdownContainer" class="dropdown-container">
                            {% for fun in toFun %}
                                {% if fun.funcName != "查看单个属性" %}
                                    <div class="menu-item" data-id="{{ fun.Identifier }}">
                                        <span>{{ fun.funcName }}</span>
                                        <div class="sub-menu">
                                            <div class="sub-option" data-type=1>查看上报数据</div>
                                            <div class="sub-option" data-type=0>查看接收数据</div>
                                        </div>
                                    </div>
                                {% endif %}
                            {% endfor %}
                        </div>
                      </div>
                </div>
            </div>
        </div>
    </div>

    </div>

<!-- 小模态框:有必填项没填 -->
<div class="modal fade" id="mySmallModal" tabindex="-1" role="dialog" aria-labelledby="mySmallModalLabel">
  <div class="modal-dialog modal-sm" role="document">
    <div class="modal-content border-5">
        <div id="mySmallModalDiv" type="button" class="btn btn-danger" data-dismiss="modal" onclick="canceDelete('mySmallModal')"></div>
    </div>
  </div>
</div>

{% endblock %}


{% block js %}

{#<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>#}
<script src="{% static '/js/echarts.min.js' %}"></script>

<script>
{#let myChart = null; // 在全局或模块作用域声明#}

class DeviceDataWebSocket {
    constructor(params) {
        // 状态管理
        this.isDestroyed = false;
        this.reconnectTimer = null; // 新增重连定时器引用

        // 1. 初始化唯一标识
        this.instanceId = `chart-${Date.now()}`;

        // 2. 延迟DOM操作（确保DOM就绪）
        setTimeout(() => this.initComponents(params), 0);
    }

    initComponents(params) {
        // 3. 设置DOM标识
        this.chartDom = document.getElementById('lineChart');
        if (!this.chartDom) throw new Error("DOM元素未找到");
        this.chartDom.dataset.instanceId = this.instanceId;

        // 4. 安全清理
        this.cleanupChart();

        // 5. 初始化WebSocket
        this.initWebSocket(params);

        // 6. 初始化图表
        this.initChart();
    }

    initWebSocket(params) {
        // 关闭旧连接（新增）
        if (this.socketChart) {
          this.socketChart.close();
          this.socketChart = null;
        }
        // 创建新连接
        this.socketChart = new WebSocket(`${wsProtocol}${SOCKETIO_SERVER_URL}/api/device-data/?` +
          `device_name=${encodeURIComponent(params.device_name)}&` +
          `product_key=${encodeURIComponent(params.product_key)}`);

        // 7. 完善事件监听
        this.socketChart.onopen = () => console.log("WebSocket连接已建立");
        this.socketChart.onerror = (e) => console.error("WebSocket错误:", e);
        this.socketChart.onmessage = (e) => this.handleMessage(e);
        this.socketChart.onclose = (e) => {
           console.log(e.wasClean ? "正常关闭" : "异常断开");
           {#this.cleanupChart();#}
          // 异常重连逻辑
          if (!e.wasClean && !this.isDestroyed) { // 新增销毁检查
            this.reconnectTimer = setTimeout(() => {
              {#this.initWebSocket(params);#}
              this.initComponents(params);
            }, 3000);
          }
        };
    }

    // 8.初始化图表实例
    initChart() {
        // 8. 安全获取实例
        const existing = echarts.getInstanceByDom(this.chartDom);
        this.myChart = existing && !existing.isDisposed() ?
            existing :
            echarts.init(this.chartDom);
    }

    // 9. 增强的清理方法
    cleanupChart() {
      if (!this.chartDom || !document.body.contains(this.chartDom)) return;

      // 1. 清理非本实例的残留
      if (this.chartDom.dataset.instanceId !== this.instanceId) {
        const oldInstance = echarts.getInstanceByDom(this.chartDom);
        if (oldInstance && !oldInstance.isDisposed()) {
          oldInstance.dispose();
        }
      }

      // 2. 清理当前实例
      if (this.myChart && !this.myChart.isDisposed()) {
        this.myChart.dispose();
      }

      // 3. 重置引用和标识 (新增)
      this.myChart = null;
      delete this.chartDom.dataset.instanceId; // 关键清理
    }

    // 10. 完整的销毁方法
    destroy() {
        this.isDestroyed = true; // 新增销毁标志
        {#clearTimeout(this.reconnectTimer); // 清除重连#}
        // 1. 清理图表
        this.cleanupChart();

        // 2. 关闭WebSocket连接
        if (this.socketChart) {
          // 移除事件监听防止内存泄漏
          this.socketChart.onopen = null;
          this.socketChart.onmessage = null;
          this.socketChart.onerror = null;
          this.socketChart.onclose = null;

          // 安全关闭连接
          if (this.socketChart.readyState === WebSocket.OPEN) {
            this.socketChart.close(1000, "Component destroyed");
          }
          this.socketChart = null;
        }

        // 3. 清除重连定时器 (新增)
        clearTimeout(this.reconnectTimer);
    }

    handleMessage(event) {
        try {
            const { type, version, data } = JSON.parse(event.data);
            switch(type) {
                case 'initial_data':
                case 'cached_data':  // 这两个case共享同一段代码
                    // 统一处理初始数据和缓存数据
                    this.currentVersion = version;
                    this.renderChart(data);
                    break;

                case 'realtime_update':
                    // 仅当新版本更高时更新
                    if (version > this.currentVersion) {
                        this.currentVersion = version;
                        this.renderChart(data);
                    }
                    break;
                case 'filtered_data':  // 新增处理类型:点击按钮查看
                    this.currentData = data;
                    this.renderChart(data);
                    break;
            }
        } catch (e) {
            console.error('WebSocket消息处理失败:', e);
        }
    }

    // 实现图表渲染逻辑
    {#renderChart(data) { websockData(data) }#}

    // 图表渲染逻辑
    renderChart(data) {
        // 确保图表实例存在
        if (!this.myChart || this.myChart.isDisposed()) {
            this.myChart = this.initChart();
        }

        // 实际渲染逻辑
        {#const option = this.processData(data);#}
        {#this.myChart.setOption(option); //#}
        this.websockData(data)
    }

    // 实现图表增量更新
    {#updateChart(newItems) {websockData(newItems)} #}

    // 按钮触发请求
    requestData(params) {
        this.socketChart.send(JSON.stringify({
            action: 'filter_data', // 新增动作类型
            payload: params        // 参数统一放在payload中
        }));
    }


    //6.显示折线图
    websockData(data){
        // 初始化图表
        if(!this.chartDom){
            this.chartDom = document.getElementById('lineChart');
            if (!this.chartDom) throw new Error("DOM元素未找到");
            this.chartDom.dataset.instanceId = this.instanceId;
        }
        if (!this.myChart || this.myChart.isDisposed()) {
            this.myChart = this.initChart();
        }
        {#const chartDom = document.getElementById('lineChart');#}
        {#if (!this.myChart) {#}
        {#    this.myChart = echarts.init(chartDom); // 仅初始化一次#}
        {#  }#}

        {#const myChart = echarts.init(chartDom);#}
        // 处理数据
        const processData = (rawData) => {
            const series = [];
            const timeSet = new Set();
            // 把时间戳加到timeSet集合里(自动去重)
            for (const deviceData of Object.values(rawData)) {
                for (const value of Object.values(deviceData)) {
                    timeSet.add(value.time);
                }
            }

            // 排序
            const sortedTimes = Array.from(timeSet).sort((a, b) => a - b);
            // 格式时间:将时间戳生成"2025/7/12 14:30:25"
            const formatTime = (timestamp) => {
                return new Date(timestamp).toLocaleString();
            };

            // Bootstrap主题颜色
            const colors = ['#0d6efd', '#fd7e14', '#20c997', '#6f42c1', '#d63384'];

            // 生成设备对应要的time和value
            Object.keys(rawData).forEach((deviceName, index) => {
                const deviceData = rawData[deviceName];
                {#console.log('deviceData:', deviceData)#}
                const valueMap = new Map();
                Object.values(deviceData).forEach(item => {
                    valueMap.set(item.time, item.value);
                });
                series.push({
                    name: deviceName,
                    type: 'line',
                    data: sortedTimes.map(time => ({
                        value: valueMap.get(time) || null,
                        time: formatTime(time)
                    })),
                    symbol: 'circle',   //定义数据点的标记形状为圆形（其他可选值：'rect'矩形、'triangle'三角形等）
                    symbolSize: 8,  //设置数据点标记的尺寸为8像素
                    itemStyle: {  //控制数据点标记的填充颜色
                        color: colors[index % colors.length]
                    },
                    lineStyle: {
                        width: 3,  //设置折线宽度为3像素
                        color: colors[index % colors.length]
                    }
                });
            });
            return {
                times: sortedTimes.map(formatTime),
                series: series
            };
        };


        // 设置图表选项
        const {times, series} = processData(data);
        const option = {
        title: {
            text: '设备数据趋势图',
            left: 'center',
            textStyle: {
                {#color: '#495057',#}
                color: '#6B7883',
                fontWeight: '500'
            }
        },
        tooltip: {
            trigger: 'axis', //表示提示框触发方式为坐标轴触发（悬浮时显示所有系列在该位置的数据）,'axis'时：params为数组, 'item'时：params为单个数据项对象
            backgroundColor: 'rgba(255, 255, 255, 0.95)',
            borderColor: '#dee2e6',
            borderWidth: 1,
            textStyle: {color: '#212529' },
            formatter: params => {  //params来源于series.data
                let result = `<div class="p-2"><strong>${params[0].axisValue}</strong><br/>`;
                params.forEach(param => {
                    result += `<span style="display:inline-block;margin-right:5px;border-radius:10px;width:10px;height:10px;background-color:${param.color};"></span>
                              ${param.seriesName}: <strong>${param.value !== null ? param.value : '无数据'}</strong><br/>`;
                });
                return result + '</div>';
            }
        },
        legend: {  // 图例组件
            data: Object.keys(data), //例如若data={A:[1,2], B:[3,4]}，则生成['A','B']两个图例项
            bottom: 10, //其他定位方式：top/left/right也可组合使用
            textStyle: {color: '#6c757d'} //还可定义fontSize/fontWeight等属性
        },
        grid: { //折线图的网格系统
            left: '3%', //图表区域距容器左侧保留3%空间
            right: '4%', //右侧保留4%空间
            bottom: '15%',
            top: '15%', //上下各保留15%空间（常用于放置标题或图例）
            containLabel: true //确保坐标轴标签完整显示在容器内;设为false时：网格尺寸仅包含坐标轴线; 设为true时：网格区域会扩展至包含坐标轴标签
        },
        xAxis: {
            type: 'category', //声明为类目轴，适用于离散数据（如时间点、分类名称）
            name: '时间',
            nameLocation: 'end', //（可选start/middle）
            nameTextStyle: {
              color: '#6c757d',
              fontWeight: 'bold',
              padding: [5, 0, 0, 0]
            },
            {#boundaryGap: false, //取消轴线两端留白，使折线紧贴Y轴#}
            boundaryGap: [0, 0.05], // 左右各留5%空白
            data: times, //绑定数据源
            axisLine: {
                show: true,
                lineStyle: {color: '#dee2e6'}, //设置轴线颜色为浅灰色(#dee2e6
                // 添加箭头配置（x轴箭头在右侧）
                symbol: ['none', 'arrow'],
                symbolSize: [10, 15],
                symbolOffset: [10, 0]  // 水平方向微调
            },
            axisLabel: {  //刻度标签样式
                color: '#6c757d',
                rotate: 45  //标签旋转45度防重叠
            }
        },
        yAxis: {
            type: 'value',  //声明为数值轴，适用于连续数值数据（区别于类目轴）
            name: '数值',
            boundaryGap: [0, 0.1], //箭头位置比最大值多出10%
            {#interval: 0.5, // 每0.5一个刻度#}
            nameTextStyle: { //控制坐标轴标题样式
              color: '#6c757d',
              fontWeight: 'bold',
              padding: [0, 0, 5, 0]
            },
            axisLine: {
                show: true,  // 显式设置为true确保显示
                lineStyle: {color: '#dee2e6'},  //设置轴线颜色为浅灰色(#dee2e6)
                // 添加箭头配置（y轴箭头在顶部）
                symbol: ['none', 'arrow'], // [起点符号，终点符号]
                symbolSize: [10, 15],      // 箭头尺寸 [宽, 高]
                symbolOffset: [0, 10]      // 箭头位置微调
            },
            axisLabel: {color: '#6c757d'}, //刻度标签文字颜色(#6c757d)
            splitLine: {
                lineStyle: {color: '#333'} //设置横向分割线颜色为极浅灰色(#f8f9fa)
            }
        },
        series: series
    };

        this.myChart.setOption(option, true);  // true表示不合并旧数据,即是把旧数据剔除,重新渲染新数据

        // 响应式调整
        {#window.addEventListener('resize', function() {#}
        {#    this.myChart.resize();#}
        {# });#}
        window.addEventListener('resize', () => {
            this.myChart.resize();
        });
    }

}

document.addEventListener('DOMContentLoaded', function() {
    // 预设值请求后端参数:用的websocket
    // 请求后端需要携带的参数:device_name,product_key,sendOrReceive,Identifier(哪个属性)
    let params = {
        device_name: "{{ dict_iot.device_name|escapejs }}",
        product_key: "{{ dict_iot.product_key|escapejs }}",
        sendOrReceive: 0, //默认先看接收成功的
        Identifier: '' //单个的属性名
    };
    // 按钮事件绑定:查看上报数据
    document.getElementById('reportDataBtn').addEventListener('click', () => {
      params['sendOrReceive'] = 1  // 数据库规定,1为上报成功的
      params['Identifier'] = ''   // 所有属性名
      wsHandler.requestData(params);
    });

    // 查看接收数据
    document.getElementById('receiveDataBtn').addEventListener('click', () => {
      params['sendOrReceive'] = 0  // 数据库规定,0为接收成功的
      params['Identifier'] = ''   // 所有属性名
      wsHandler.requestData(params);
    });

    // 1. 初始按钮点击事件 - 显示上拉菜单
    document.getElementById('toggleMenuBtn').addEventListener('click', function(e) {
        const container = document.getElementById('dropdownContainer');
        container.style.display = 'block';
        // 关键修改：计算按钮上方位置
        const rect = e.target.getBoundingClientRect();
        const containerHeight = container.offsetHeight; // 获取菜单高度
        // 定位公式：按钮顶部位置 - 菜单高度 - 间距
        container.style.top = `${rect.top - containerHeight - 5}px`;
        container.style.left = `${rect.left}px`;
        {#    // 定位在按钮下方#}
        {#    const rect = e.target.getBoundingClientRect();#}
        {#    container.style.top = `${rect.bottom + 5}px`;#}
        {#    container.style.left = `${rect.left}px`;#}
    });

    // 2. 子菜单点击处理
    document.querySelectorAll('.sub-option').forEach(option => {
        option.addEventListener('click', function() {
            // 获取主选项ID和操作类型
            const parentItem = this.closest('.menu-item');
            {#const mainOptionId = parentItem.dataset.id;#}
            {#const actionType = this.dataset.type;#}
            // 执行操作（示例：控制台输出）
            {#console.log(`主选项ID: ${mainOptionId}, 操作类型: ${actionType}`);#}
            params['sendOrReceive'] = this.dataset.type
            params['Identifier'] = parentItem.dataset.id
            wsHandler.requestData(params);
            // 隐藏下拉菜单
            document.getElementById('dropdownContainer').style.display = 'none';
        });
    });

    // 3. 点击页面其他区域关闭菜单
    document.addEventListener('click', function(e) {
        const container = document.getElementById('dropdownContainer');
        if (!container.contains(e.target) && e.target.id !== 'toggleMenuBtn') {
            container.style.display = 'none';
        }
    });

    // 实例化设备数据推送服务
    const wsHandler = new DeviceDataWebSocket(params);

    // 组件卸载时显式调用销毁
    // 11. 生命周期管理
    document.addEventListener('visibilitychange', () => {
        if (document.visibilityState === 'hidden') {
            wsHandler.cleanupChart();
            wsHandler.socketChart?.close();
        } else if (document.visibilityState === 'visible') {
            wsHandler.initComponents(params); // 重新连接方法
        }
     });

    // 监听页面卸载
    window.addEventListener('beforeunload', () => wsHandler.destroy());

})

    // 全局变量
    DEVICENAME = '{{ alias.device_name }}'
    SOCKETIO_SERVER_URL = '{{ SOCKETIO_SERVER_URL }}'
    // 字符串转为数字相应类型
    function strChangeNum(str){
       if(/^-?\d+$/.test(str)){
           return parseInt(str, 10)
       }else {
           return parseFloat(str)
       }
    }

    // 如果是数字类型获得焦点或者点击进来,各数值的判断与逻辑检查
    function sliderValue(mid, mpm = '', max = '', min = '', step = '') {
        let slider = $('#slider' + mid);
        let input = $('#input-slider' + mid);
        slider.on('input change', function () {
            input.val(this.value);
        });
        input.on('input change', function () {
            slider.val(this.value);
        });

        if (mpm === '+') {
            let val = strChangeNum(input.val()) + strChangeNum(step);

            // 如果val是浮点数,则去取min，max，step的小数位，转化为跟三者最长的小数位一样
            if (val % 1 === 0){}else{
                var dice_min = min.split('.').length === 2 ? min.split('.')[1].length : 0
                var dice_max = max.split('.').length === 2 ? max.split('.')[1].length : 0
                var dice_step = step.split('.').length === 2 ? step.split('.')[1].length : 0
                var max_min = dice_min > dice_max ? dice_min : dice_max
                var dice = max_min > dice_step ? max_min : dice_step
                val = parseFloat(val.toFixed(dice))
            }

            if (val > strChangeNum(max)) {
                val = strChangeNum(max);
                $('#error1').text('最大值为:' + max)
                 deleteAddAction('mySmallModal',0, '最大值为:' + max) //进入模态框
            } else {
                $('#error1').text('')
            }
            slider.val(val);
            input.val(val);
        }
        if (mpm === '-') {
            let val = strChangeNum(input.val()) - strChangeNum(step);

            // 如果val是浮点数,则去取min，max，step的小数位，转化为跟三者最长的小数位一样
            if (val % 1 === 0){}else{
                var dice_min = min.split('.').length === 2 ? min.split('.')[1].length : 0
                var dice_max = max.split('.').length === 2 ? max.split('.')[1].length : 0
                var dice_step = step.split('.').length === 2 ? step.split('.')[1].length : 0
                var max_min = dice_min > dice_max ? dice_min : dice_max
                var dice = max_min > dice_step ? max_min : dice_step
                val = parseFloat(val.toFixed(dice))
            }

            if (val < strChangeNum(min)) {
                val = strChangeNum(min);
                $('#error1').text('最小值为:' + min)
                deleteAddAction('mySmallModal',0, '最小值为:' + min) //进入模态框
            } else {
                $('#error1').text('')
            }
            slider.val(val);
            input.val(val);
        }
    }

    // input失去焦点,检查输入值是否在max-min之间
    function inputOnblur(mid, mpm = '', max = '', min = '', value = '', step='') {
        let input = $('#input-slider' + mid);
        {#if ((strChangeNum(value) - strChangeNum(input.val())) % strChangeNum(step) === 0){#}
            {#console.log('整除')#}
        {# }else {#}
            {#console.log('不整除');#}
        {#    $('#error1').text('输入值应按照步长' + step + '来加减');#}
            {#alert('输入值应按照步长' + step + '来加减')#}
        {#    deleteAddAction('mySmallModal',0, '输入值应按照步长' + step + '来加减') //进入模态框#}
        {#    setTimeout(function (){input.val(value)},5000)#}
        {#    input.focus()#}
        {#    return#}
        {# }#}
        if (input.val() >= strChangeNum(min) && input.val() <= strChangeNum(max)) {
            $('#error1').text('')
        } else {
            $('#error1').text('输入值应在:' + min + '-' + max + '之间')
            setTimeout(function (){input.val(value)},5000)
            deleteAddAction('mySmallModal',0, '输入值应在:' + min + '-' + max + '之间') //进入模态框
            input.focus()
        }
    }

    // 移动滑块时,input框同步数值(暂时不用了)
    {#function updateSliderValue(value, mid) { document.getElementById(mid).innerText = value; }#}

    // 页面加载完毕，执行websocket, 根据当前页面协议自动切换
    const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
    let socket = new WebSocket(wsProtocol+SOCKETIO_SERVER_URL + "/room/" + DEVICENAME + "/?deviceName=" + DEVICENAME);
    // 创建好连接之后自动触发（服务器执行self.accept()后）
    socket.onopen = function (event) { connectInfor(' (连接中)') }  // 主题加上连接状态

    // 点击的按钮：发送消息给服务端
    function sendAction(index) {
        // 获取多选框组的值
        const allCheckboxes = Array.from( document.querySelectorAll('input[name="language"]') );
        const checkboxData = allCheckboxes.map((checkbox, index) => ({
            index: index,            // 索引位置
            checked: checkbox.checked, // 是否选中
        }));
        {#console.log('checkboxData:',checkboxData)#}

        // 获取后端传来的设备各项身份信息
        let dictIot = '{{ dict_iot }}'.replaceAll('&#x27;', '').replaceAll('{', '').replaceAll('}', '').split(',')
        // 遍历整个列表，转为字典
        let IotData = {}
        Object.keys(dictIot).forEach((key) => {
            IotData[dictIot[key].split(':')[0].trim()] = dictIot[key].split(':')[1].trim()
        })
        // 获取form各项数据
        const formData_ = getFormData('funData'); // 'funData' 是表单的id
        const formData = trimFormData(formData_);
        {#console.log('formData:',formData)#}
        // 发送到后端: 表单数据，Iot数据，index表示上报物模型还是设备给设备，选择哪些发送否
        socket.send(JSON.stringify({"formData": formData, "IotData": IotData, "selectSend": index, 'checkboxData': checkboxData}))
        $('#error1').text('正在发送数据，请稍等...')
        setTimeout(function () { $('#error1').text('') }, 15000)
    }

    // 去掉form各项数据前后空格
    function trimFormData(formData) {
      const trimmedData = {};
      Object.keys(formData).forEach(key => {
        const newKey = key.trim();
        trimmedData[newKey] = typeof formData[key] === 'string'
          ? formData[key].trim()
          : formData[key];
      });
      return JSON.parse(
        JSON.stringify(trimmedData)
          .replace(/"\s*:\s*/g, '":')
      );
     }

    // 服务端有发来消息，自动触发
    socket.onmessage = function (event) {
        //1. 先把字符串转为JSON格式真正的字典
        let dictionary = JSON.parse(event.data)
        let stateMid = dictionary.hasOwnProperty('stateMid')
        {#console.log('stateMid:', stateMid)#}
        // 如果返回数据里有stateMid字段，则是先提示是否发送成功
        // 如果返回数据里没有stateMid字段，则是已经更新增加数据库数据了,提示数据已经更新了
        if (stateMid) {
            if (dictionary.stateMid === 0) {
                $('#error1').text('已上报给物模型,正在等待返回信息,数值不闪烁就是物联网平台没有返回数据,可重试...')
                {#setTimeout(function () {$('#error1').text('已上报给物模型,正在等待返回信息...')}, 3000)#}
                {#setTimeout(function () {$('#error1').text('平台没有返回数据!')}, 3000)#}
            } else {
                if(dictionary.stateMid === 2000){
                    $('#error1').text('已上报给设备, 设备状态:[ '+dictionary.status+' ]')  // 以后再做验证功能有需要的话
                    {#setTimeout(function () {$('#error1').text('')}, 5000)#}
                }else{
                    $('#error1').text('发布失败，请再报一次')
                    {#setTimeout(function () {$('#error1').text('')}, 3000)#}
                }
            }
        } else {
            $('#error1').text('正在更新数据')
            //2. 遍历整个字典的键值对
            Object.keys(dictionary).forEach((key) => {
                //3. 按照key定位到界面位置,再显示出来
                let mkey = ['input-slider' + key, 'select' + key, key]
                let value = dictionary[key].value
                Object.keys(mkey).forEach((key1) => {
                    let key2 = document.getElementById(mkey[key1])
                    if (key2) {
                        if (mkey[key1].indexOf('select') !== -1) {
                            let options = key2.options;
                            // 遍历所有的<option>元素，并将返回的值做为选中
                            for (let i = 0; i < options.length; i++) {
                                if (parseInt(value, 10) === parseInt(String(options[i].label).split('-')[0], 10)) {
                                    key2.options[i].selected = true
                                }
                            }
                        } else {
                            key2.value = value
                        }
                        // 接收到的数据显示为红色, 且3秒后，恢复原来的颜色
                        {#key2.style.color = 'red'#}
                        asyncBlink(key2, 'red', 1000)
                        setTimeout(function () {$('#error1').text('')}, 5000)
                    }
                })
            });
        }
    }

    // 异步函数（闪烁效果）
    async function asyncBlink(element, color = 'red', heartTime) {
        element.style.color = color;
        await new Promise(r => setTimeout(r, heartTime));
        element.style.color = '';
        await new Promise(r => setTimeout(r, heartTime));
        element.style.color = color;
        await new Promise(r => setTimeout(r, heartTime));
        element.style.color = '';
        await new Promise(r => setTimeout(r, heartTime));
        element.style.color = color;
        await new Promise(r => setTimeout(r, heartTime));
        element.style.color = '';

    }

    // 服务器主动断开连接时，客户端触发onclose回调函数
    socket.onclose = function (event) {connectInfor(' (断开了)')}   // 主题加上连接状态

    // 出错信息
    socket.onerror = function (error) {connectInfor(' (出错了)')};  // 主题加上连接状态

    // 关闭连接
    {#socket.close()#}

    // 主题加连接信息函数
    function connectInfor(infor) {
        // 使用innerHTML添加文字, 因要刷新html,性能会差一点
        let innerHTML = document.getElementById('textInfo').innerHTML
        innerHTML = innerHTML.split('(')[0] + infor
        document.getElementById('textInfo').innerHTML = innerHTML
        // 使用insertAdjacentHTML添加文字
        {#document.getElementById('textInfo').insertAdjacentHTML('afterend', '(连接中)');#}
    }

    // 获取表单中的各项数据函数
    function getFormData(formId) {
        const form = document.forms[formId];
        const formData = {};

        for (let i = 0; i < form.elements.length; i++) {
            const element = form.elements[i];
            if (element.name === 'waitSubmit') {
                let dataId = element.id.replace('input-slider', '').replace('select', '')
                switch (element.type) {
                    case 'radio':
                    case 'checkbox':
                        if (element.checked) {
                            formData[dataId] = element.value.split('~')[0]; <!--好像不用执行这里，默认值就是取选中的-->
                        }
                        break;
                    case 'file':
                        formData[dataId] = element.files[0];
                        break;
                    default:
                        formData[dataId] = element.value.split('~')[0];
                }
            }
        }
        return formData;
    }

    // mymo是哪个modal要显示：像'myModal'等， med是哪个id要被关联后边删除用到，mts是提示信息
    function deleteAddAction(mymo, med, mts = '') {
        if (mts !== '') {
            // 获取div元素，作为提示框显示提示信息用
            var div = document.getElementById('mySmallModalDiv');
            // 给div传值并显示div.innerHTML = '请注意, 前面有必填项没填！';// 或者使用
            div.textContent = mts;  // 这个直接渲染文本，不带样式，安全一点
        }
        // 调用确认是哪里调用的模态框
        $('#' + mymo).modal('show');
        // 定时关闭小模态框(消息提示)
        if (mymo === 'mySmallModal') {
            setTimeout(function () {
                $('#' + mymo).modal('hide');
                $('#error1').text('')
            }, 5000)
        }
    }

</script>
{% endblock %}

